<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\QrCode;

use Exception;

class QrInput
{
    public array $items = [];
    private int $version = 0;
    private int $level = 0;

    public function __construct(int $version = 0, int $level = QR_EC_LEVEL_L)
    {
        $this->version = $version;
        $this->level = $level;
    }

    public function getVersion(): int
    {
        return $this->version;
    }

    /**
     * @param $version
     * @return int
     * @throws Exception
     * setVersion
     */
    public function setVersion($version): int
    {
        if ($version < 0 || $version > QR_SPEC_VERSION_MAX) {
            throw new Exception('Invalid version no');
        }
        $this->version = $version;
        return 0;
    }

    public function getErrorCorrectionLevel(): int
    {
        return $this->level;
    }

    public function append($mode, $size, $data): int
    {
        try {
            $entry = new QrInputItem($mode, $size, $data);
            $this->items[] = $entry;
            return 0;
        } catch (Exception $e) {
            return -1;
        }
    }

    public static function checkModeNum($size, $data): bool
    {
        for ($i = 0; $i < $size; $i++) {
            if ((ord($data[$i]) < ord('0')) || (ord($data[$i]) > ord('9'))) {
                return false;
            }
        }
        return true;
    }

    public static function estimateBitsModeNum($size)
    {
        $w = intval($size / 3);
        $bits = $w * 10;
        switch ($size - $w * 3) {
            case 1:
                $bits += 4;
                break;
            case 2:
                $bits += 7;
                break;
            default:
                break;
        }
        return $bits;
    }

    public static array $anTable = [
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43,
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1,
        -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
        25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
    ];

    public static function lookAnTable($c): int
    {
        return (($c > 127) ? -1 : self::$anTable[$c]);
    }

    public static function checkModeAn($size, $data): bool
    {
        for ($i = 0; $i < $size; $i++) {
            if (self::lookAnTable(ord($data[$i])) == -1) {
                return false;
            }
        }
        return true;
    }

    public static function estimateBitsModeAn($size)
    {
        $w = intval($size / 2);
        $bits = $w * 11;
        if ($size & 1) {
            $bits += 6;
        }
        return $bits;
    }

    public static function estimateBitsMode8($size)
    {
        return $size * 8;
    }

    public static function estimateBitsModeKanji($size): int
    {
        return intval(($size / 2) * 13);
    }

    public static function checkModeKanji($size, $data): bool
    {
        if ($size & 1) return false;
        for ($i = 0; $i < $size; $i += 2) {
            $val = (ord($data[$i]) << 8) | ord($data[$i + 1]);
            if ($val < 0x8140 || ($val > 0x9ffc && $val < 0xe040) || $val > 0xebbf) {
                return false;
            }
        }
        return true;
    }

    public static function check($mode, $size, $data): bool
    {
        if ($size <= 0) return false;
        switch ($mode) {
            case QR_MODE_NUM:
                return self::checkModeNum($size, $data);
            case QR_MODE_AN:
                return self::checkModeAn($size, $data);
            case QR_MODE_KANJI:
                return self::checkModeKanji($size, $data);
            case QR_MODE_8:
            case QR_MODE_STRUCTURE:
                return true;
        }
        return false;
    }

    public function estimateBitStreamSize($version): int
    {
        $bits = 0;
        foreach ($this->items as $item) {
            if ($item instanceof QrInputItem) $bits += $item->estimateBitStreamSizeOfEntry($version);
        }
        return $bits;
    }

    public function estimateVersion(): int
    {
        $version = 0;
        do {
            $prev = $version;
            $bits = $this->estimateBitStreamSize($prev);
            $version = QrSpec::getMinimumVersion(intval(($bits + 7) / 8), $this->level);
            if ($version < 0) return -1;
        } while ($version > $prev);
        return $version;
    }

    public function createBitStream(): int
    {
        $total = 0;
        foreach ($this->items as $item) {
            if (!($item instanceof QrInputItem)) return -1;
            $bits = $item->encodeBitStream($this->version);
            if ($bits < 0) return -1;
            $total += $bits;
        }
        return $total;
    }

    /**
     * @return int
     * @throws Exception
     * convertData
     */
    public function convertData(): int
    {
        $ver = $this->estimateVersion();
        if ($ver > $this->getVersion()) $this->setVersion($ver);
        for (; ;) {
            $bits = $this->createBitStream();
            if ($bits < 0) return -1;
            $ver = QrSpec::getMinimumVersion(intval(($bits + 7) / 8), $this->level);
            if ($ver < 0) {
                throw new Exception('WRONG VERSION');
            } else if ($ver > $this->getVersion()) {
                $this->setVersion($ver);
            } else {
                break;
            }
        }
        return 0;
    }

    /**
     * @param QRbitstream $bstream
     * @return int
     * appendPaddingBit
     */
    public function appendPaddingBit(QRbitstream $bstream): int
    {
        $bits = $bstream->size();
        $maxwords = QrSpec::getDataLength($this->version, $this->level);
        $maxbits = $maxwords * 8;
        if ($maxbits == $bits) {
            return 0;
        }
        if ($maxbits - $bits < 5) {
            return $bstream->appendNum($maxbits - $bits, 0);
        }
        $bits += 4;
        $words = intval(($bits + 7) / 8);
        $padding = new QRbitstream();
        $ret = $padding->appendNum($words * 8 - $bits + 4, 0);
        if ($ret < 0) return $ret;
        $padlen = $maxwords - $words;
        if ($padlen > 0) {
            $padbuf = [];
            for ($i = 0; $i < $padlen; $i++) {
                $padbuf[$i] = ($i & 1) ? 0x11 : 0xec;
            }
            $ret = $padding->appendBytes($padlen, $padbuf);
            if ($ret < 0) return $ret;
        }
        return $bstream->append($padding);
    }

    /**
     * @return QRbitstream|null
     * @throws Exception
     * mergeBitStream
     */
    public function mergeBitStream(): ?QRbitstream
    {
        if ($this->convertData() < 0) return null;
        $bstream = new QRbitstream();
        foreach ($this->items as $item) {
            $bstream->append($item->bstream);
        }
        return $bstream;
    }

    /**
     * @return QRbitstream|null
     * @throws Exception
     * getBitStream
     */
    public function getBitStream(): ?QRbitstream
    {
        $bstream = $this->mergeBitStream();
        if ($bstream == null) return null;
        $ret = $this->appendPaddingBit($bstream);
        if ($ret < 0) return null;
        return $bstream;
    }

    /**
     * @return array|null
     * @throws Exception
     * getByteStream
     */
    public function getByteStream(): ?array
    {
        $bstream = $this->getBitStream();
        if ($bstream == null) return null;
        return $bstream->toByte();
    }
}